package com.onlocation.services;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.Vibrator;
import android.widget.Toast;
import com.onlocation.observers.AbstractCustomContentObserver;
import com.onlocation.observers.MissedCallsContentObserver;
import com.onlocation.observers.MissedMsContentObserver;
import com.onlocation.db.DatabaseHelper;
import com.onlocation.gui.Main;
import com.onlocation.objects.Coordinate;
import com.onlocation.R;
import com.onlocation.receivers.AbstractAlarmReceiver;
import com.onlocation.receivers.LocationAlarmReceiver;
import com.onlocation.receivers.NotificationAlarmReceiver;
import com.onlocation.util.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Author: Steve Carrell
 * Date: 5/12/12
 */
public class LocatorService extends Service {
    public static final String CLASS_NAME = LocatorService.class.getName();
    public static final int FLAG_ABORT = 1;
    public static final int FLAG_UPDATE = 2;
    public static final int FLAG_NEW_DATA = 4;
    public static final int FLAG_LOC_TIMER_TICK = 8;
    public static final int FLAG_NOTIF_TIMER_TICK = 16;
    public static final String BROADCAST_ACTION = LocatorService.class.getName() + "_BROADCAST";
    public static final String DEFAULT = ResourceUtil.getString(R.string.defaultLocation);
    public static final int NOTIF_ID = 1;

    private static String currentName = "";

    private final LocalLocationListener gpsListener = new LocalLocationListener();
    private final LocalLocationListener networkListener = new LocalLocationListener();
    private final DatabaseHelper db = DatabaseHelper.getInstance(this);
    private final AbstractAlarmReceiver locTimer = new LocationAlarmReceiver();
    private final AbstractAlarmReceiver notifTimer = new NotificationAlarmReceiver();

    private Map<String, Coordinate> locationMap = new HashMap<String, Coordinate>();
    private Location currentLocation = null;
    private boolean isRestarting = false;
    private boolean isNotifDisplayed = false;
    private LocationManager locationManager;
    private int locDelay = 0;
    private int notifDelay = AbstractCustomContentObserver.UPDATE_NEVER;
    private boolean useGps = true;
    private List<AbstractCustomContentObserver> contentObservers =
            new ArrayList<AbstractCustomContentObserver>();

    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            int flag = intent.getExtras().getInt("flag", 0);
            Logger.d(String.format("received flag: %d", flag), CLASS_NAME);

            switch (flag) {
                case LocatorService.FLAG_NEW_DATA:
                    buildLocationMap();
                    break;
                case LocatorService.FLAG_LOC_TIMER_TICK:
                    doLocAlarmAction();
                    break;
                case LocatorService.FLAG_NOTIF_TIMER_TICK:
                    doNotifyMissedCall();
                    break;
                default:
                    break;
            }
        }
    };

    private OnSharedPreferenceChangeListener prefListener = new OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            if (StringUtil.equalsIgnoreNull(key,
                    ResourceUtil.getString(R.string.pref_key_refresh_rate))) {
                // Update location timer.
                locTimer.stopTimer(getBaseContext());
                locDelay = PreferenceUtil.getRefreshRate();
                locTimer.startTimer(getBaseContext(), locDelay);
            } else if (StringUtil.equalsIgnoreNull(key,
                    ResourceUtil.getString(R.string.pref_key_missed_calls)) ||
                    StringUtil.equalsIgnoreNull(key,
                            ResourceUtil.getString(R.string.pref_key_notif_rate))) {
                // Update missed call notification timer.
                notifTimer.stopTimer(getBaseContext());
                notifDelay = PreferenceUtil.getMissedCallNotifRate();
                buildContentObservers();
                if (notifDelay > AbstractCustomContentObserver.UPDATE_ON_LOC_CHANGE) {
                    notifTimer.startTimer(getBaseContext(), notifDelay);
                }
            } else if (StringUtil.equalsIgnoreNull(key,
                    ResourceUtil.getString(R.string.pref_key_use_gps))) {
                // Update gps location update listener.
                useGps = !useGps;
                currentName = "";
                currentLocation = null;
                if (useGps) {
                    locationManager.requestLocationUpdates(
                            LocationManager.GPS_PROVIDER, locDelay, 0, gpsListener);
                } else {
                    locationManager.removeUpdates(gpsListener);
                }
            } else if (StringUtil.equalsIgnoreNull(key,
                    ResourceUtil.getString(R.string.pref_key_show_notif))) {
                // Display/remove persistent notification.
                setNotificationIcon(true);
            }
        }
    };

    private final class LocalLocationListener implements LocationListener {
        public void onLocationChanged(Location location) {
        }

        public void onProviderDisabled(String provider) {
            Logger.d(provider + " disabled", CLASS_NAME);
            if (!isProviderEnabled(LocationManager.GPS_PROVIDER) &&
                    !isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
                // No more location providers running, stop the service.
                doExit();
            }
        }

        public void onProviderEnabled(String provider) {
            Logger.d(provider + " enabled.", CLASS_NAME);
        }

        public void onStatusChanged(String provider, int status, Bundle extra) {
            switch (status) {
                case LocationProvider.OUT_OF_SERVICE:
                case LocationProvider.TEMPORARILY_UNAVAILABLE:
                    Logger.d(provider + " temporarily unavailable or out of service", CLASS_NAME);
                    break;
                case LocationProvider.AVAILABLE:
                    Logger.d(provider + " available", CLASS_NAME);
                    break;
                default:
                    break;
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onDestroy() {
        unregisterReceiver(receiver);
        PreferenceUtil.getPreferences().unregisterOnSharedPreferenceChangeListener(prefListener);
        locationManager.removeUpdates(gpsListener);
        locationManager.removeUpdates(networkListener);
        destroyContentObservers();
        setNotificationIcon(false);
        currentName = "";
        currentLocation = null;
        locTimer.stopTimer(this);
        notifTimer.stopTimer(this);
        super.onDestroy();
        if (!PreferenceUtil.doSilentUpdate()) {
            Toast.makeText(this, "Stopped.", Toast.LENGTH_SHORT).show();
        }
        Logger.v("destroyed", CLASS_NAME);
    }

    @Override
    public void onStart(Intent intent, int startId) {
        Logger.v("Starting LocatorService.", CLASS_NAME);
        super.onStart(intent, startId);
        
        IntentFilter intentFilter = new IntentFilter(Main.BROADCAST_ACTION);
        intentFilter.addAction(locTimer.getBroadcastAction());
        intentFilter.addAction(notifTimer.getBroadcastAction());
        registerReceiver(receiver, intentFilter);
        PreferenceUtil.getPreferences().registerOnSharedPreferenceChangeListener(prefListener);

        startupProcedure();
        setNotificationIcon(true);
        Logger.v("started", CLASS_NAME);
    }

    private void startupProcedure() {
        currentName = "";
        currentLocation = null;
        locDelay = PreferenceUtil.getRefreshRate();
        notifDelay = PreferenceUtil.getMissedCallNotifRate();
        useGps = PreferenceUtil.doUseGps();
        if (locationManager == null) {
            locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        }
        boolean gpsEnabled = useGps && isProviderEnabled(LocationManager.GPS_PROVIDER);
        boolean networkEnabled = isProviderEnabled(LocationManager.NETWORK_PROVIDER);
        buildLocationMap();
        buildContentObservers();

        if (useGps) {
            locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, locDelay, 0, gpsListener);
        }
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, locDelay, 0, networkListener);

        if (!gpsEnabled && !networkEnabled) {
            Logger.w("GPS and network both disabled", CLASS_NAME);
            if (!PreferenceUtil.doSilentUpdate()) {
                Toast.makeText(this, "Unable to get location.", Toast.LENGTH_LONG).show();
            }
            doExit();
        } else {
            if (!isRestarting && !PreferenceUtil.doSilentUpdate()) {
                Toast.makeText(this, "Started.", Toast.LENGTH_SHORT).show();
            }
            fireLocationUpdate();
            locTimer.startTimer(this, locDelay);
            if (notifDelay > AbstractCustomContentObserver.UPDATE_ON_LOC_CHANGE) {
                notifTimer.startTimer(this, notifDelay);
            }
        }
    }

    private void doExit() {
        Logger.d("stopping", CLASS_NAME);
        sendBroadcast(FLAG_ABORT);
        stopSelf();
    }

    private boolean isProviderEnabled(String provider) {
        boolean enabled;
        try {
            enabled = (locationManager.isProviderEnabled(provider));
        } catch (Exception e) {
            enabled = false;
        }
        return enabled;
    }

    public void fireLocationUpdate() {
        boolean gpsEnabled = useGps && isProviderEnabled(LocationManager.GPS_PROVIDER);
        boolean networkEnabled = isProviderEnabled(LocationManager.NETWORK_PROVIDER);
        Location gpsLoc, networkLoc;

        if (gpsEnabled) {
            gpsLoc = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        } else {
            gpsLoc = null;
        }
        if (networkEnabled) {
            networkLoc = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        } else {
            networkLoc = null;
        }

        if (gpsLoc != null && networkLoc != null) {
            if (CoordinateUtil.isBetterLocation(gpsLoc, networkLoc)) {
                updateLocation(gpsLoc);
            } else {
                updateLocation(networkLoc);
            }
        } else if (gpsLoc != null) {
            updateLocation(gpsLoc);
        } else if (networkLoc != null) {
            updateLocation(networkLoc);
        } else {
            currentLocation = null;
        }
    }

    private void updateLocation(Location location) {
        String newName = DEFAULT;
        boolean isNewStart = currentLocation == null;
        boolean isRefresh = currentLocation == location;
        
        if (location == null ||
                (!isRefresh && !CoordinateUtil.isBetterLocation(location, currentLocation))) {
            return;
        }
        currentLocation = location;

        // get current lat/long
        double currLongitude = location.getLongitude();
        double currLatitude = location.getLatitude();
        // add accuracy to coordinate.getRadius in the following loop to get an
        // acceptable area around our given coordinate.
        float accuracy = location.getAccuracy();

        //loop through our collection of coordinates
        for (Map.Entry<String, Coordinate> entry : locationMap.entrySet()) {
            Coordinate coordinate = entry.getValue();
            if (CoordinateUtil.inSelectedArea(currLongitude, currLatitude,
                    coordinate.getLongitude(), coordinate.getLatitude(),
                    accuracy + coordinate.getRadius())) {
                newName = entry.getKey();
                break;
            }
        }

        if (isRefresh || !StringUtil.equalsIgnoreNull(currentName, newName)) {
            com.onlocation.objects.Location loc = DatabaseHelper.getInstance(this).getLocationData(newName);
            VolumeUtil.setVolumeAndVibrate(loc.getVolume(), loc.getVibrate());
            if (!PreferenceUtil.doSilentUpdate() &&
                    ((!isRefresh && !isNewStart && !isRestarting) ||
                            (isRefresh && !StringUtil.equalsIgnoreNull(currentName, newName)))) {
                // Display a toast if we are truly updating our location and not just resetting it.
                Toast.makeText(this, "Updated location: " + newName, Toast.LENGTH_LONG).show();
            }
            Logger.v("updated location: " + newName, CLASS_NAME);
            currentName = newName;
            sendBroadcast(FLAG_UPDATE);
            setNotificationIcon(true);
            if (notifDelay == AbstractCustomContentObserver.UPDATE_ON_LOC_CHANGE) {
                doNotifyMissedCall();
            }
        }
    }

    private void buildLocationMap() {
        ArrayList<String> locations = db.getLocationList();
        locationMap = new HashMap<String, Coordinate>(locations.size());
        for (String key : locations) {
            if (!key.equals(ResourceUtil.getString(R.string.defaultLocation)) &&
                    !key.equals(ResourceUtil.getString(R.string.addLocation))) {
                com.onlocation.objects.Location location = db.getLocationData(key);
                Coordinate coordinate = new Coordinate(
                        location.getLongitude(),
                        location.getLatitude(),
                        location.getRadius());
                locationMap.put(key, coordinate);
            }
        }
        if (locationManager != null) {
            updateLocation(currentLocation);
        }
    }

    private void buildContentObservers() {
        destroyContentObservers();
        if (notifDelay > AbstractCustomContentObserver.UPDATE_NEVER) {
            contentObservers.add(new MissedCallsContentObserver(this));
            contentObservers.add(new MissedMsContentObserver(this));
            for (AbstractCustomContentObserver contentObserver : contentObservers) {
                getContentResolver().registerContentObserver(contentObserver.getContentUri(), true, contentObserver);
            }
        }
    }

    private void destroyContentObservers() {
        for (AbstractCustomContentObserver contentObserver : contentObservers) {
            getContentResolver().unregisterContentObserver(contentObserver);
        }
        contentObservers.clear();
    }

    private void doNotifyMissedCall() {
        for (AbstractCustomContentObserver contentObserver : contentObservers) {
            if (contentObserver.hasMissedNotification()) {
                // Notify.
                if (VolumeUtil.getCurrentVolume() > 0) {
                    // Play notification sound.
                    Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
                    Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notification);
                    if (ringtone != null) {
                        ringtone.play();
                    }
                }
                if (VolumeUtil.getCurrentIsVibrate()) {
                    // Vibrate if set.
                    Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
                    vibrator.vibrate(VolumeUtil.VIBRATE_DURATION);
                }
                // Wake the phone up as well for a visual notification.
                PowerManager powerMgr = (PowerManager) getSystemService(Context.POWER_SERVICE);
                WakeLock wakeLock = powerMgr.newWakeLock(
                        PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP,
                        LocatorService.class.getName());
                wakeLock.acquire(5000);
                break;
            }
        }
    }

    private void setNotificationIcon(boolean enable) {
        NotificationManager notifMgr =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        if (enable && PreferenceUtil.doShowNotif()) {
            // show icon
            Intent notifIntent = new Intent(this, Main.class);
            notifIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notifIntent, 0);
            
            String contentText = (StringUtil.isNullOrEmpty(currentName)) ? "No location found." :
                    "Current Location: " + currentName;
            String tickerText = isNotifDisplayed ? "" : "On Location started.";
            
            Notification notif = new Notification(R.drawable.icon, tickerText, System.currentTimeMillis());
            notif.setLatestEventInfo(getApplicationContext(), ResourceUtil.getString(R.string.app_name),
                    contentText, contentIntent);
            notif.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
            
            notifMgr.notify(NOTIF_ID, notif);
            isNotifDisplayed = true;
        } else {
            // clear icon
            notifMgr.cancel(NOTIF_ID);
            isNotifDisplayed = false;
        }
    }

    private void doLocAlarmAction() {
        PowerManager powerMgr = (PowerManager) getSystemService(Context.POWER_SERVICE);
        WakeLock wakeLock = powerMgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LocatorService.class.getName());
        wakeLock.acquire();

        fireLocationUpdate();

        wakeLock.release();
    }
    
    private void sendBroadcast(int flag) {
        Logger.d(String.format("broadcasting flag: %d", flag), CLASS_NAME);
        Intent broadcast = new Intent();
        broadcast.putExtra("flag", flag);
        broadcast.setAction(BROADCAST_ACTION);
        sendBroadcast(broadcast);
    }

    public static String getCurrentLocation() {
        return currentName;
    }
}
